diff --git a/khal/icalendar.py b/khal/icalendar.py
index 7b5a7aefc..9844e14b4 100644
--- a/khal/icalendar.py
+++ b/khal/icalendar.py
@@ -401,6 +401,47 @@ def sanitize(vevent, default_timezone, href='', calendar=''):
     return vevent
 
 
+def sanitize_vtodo(vtodo, default_timezone, href='', calendar=''):
+    """
+    cleanup vtodos so they look like vevents for khal
+
+    :param vtodo: the vtodo that needs to be cleaned
+    :type vtodo: icalendar.cal.Todo
+    :param default_timezone: timezone to apply to start and/or end dates which
+         were supposed to be localized but which timezone was not understood
+         by icalendar
+    :type timezone: pytz.timezone
+    :param href: used for logging to inform user which .ics files are
+        problematic
+    :type href: str
+    :param calendar: used for logging to inform user which .ics files are
+        problematic
+    :type calendar: str
+    :returns: clean vtodo as vevent
+    :rtype: icalendar.cal.Event
+    """
+    vdtstart = vtodo.pop('DTSTART', None)
+    vdue = vtodo.pop('DUE', None)
+
+    # it seems to be common for VTODOs to have DUE but no DTSTART
+    # so we default to that. E.g. NextCloud does something similar
+    if vdtstart is None and vdue is not None:
+        vdtstart = vdue
+
+    # Based loosely on new_event
+    event = icalendar.Event()
+    event.add('dtstart', vdtstart)
+    event.add('due', vdue)
+    # Copy common/necessary attributes
+    for attr in ['uid', 'summary', 'dtend', 'dtstamp', 'description',
+                 'location', 'categories', 'url']:
+        if attr in vtodo:
+            event.add(attr, vtodo.pop(attr))
+
+    # Chain with event sanitation
+    return sanitize(event, default_timezone, href=href, calendar=calendar)
+
+
 def sanitize_timerange(dtstart, dtend, duration=None):
     '''return sensible dtstart and end for events that have an invalid or
     missing DTEND, assuming the event just lasts one hour.'''
diff --git a/khal/khalendar/backend.py b/khal/khalendar/backend.py
index 011f9e38c..67feb57a4 100644
--- a/khal/khalendar/backend.py
+++ b/khal/khalendar/backend.py
@@ -38,6 +38,7 @@
 from ..icalendar import assert_only_one_uid, cal_from_ics
 from ..icalendar import expand as expand_vevent
 from ..icalendar import sanitize as sanitize_vevent
+from ..icalendar import sanitize_vtodo
 from ..icalendar import sort_key as sort_vevent_key
 from .exceptions import (CouldNotCreateDbDir, NonUniqueUID,
                          OutdatedDbVersionError, UpdateFailed)
@@ -52,6 +53,11 @@
 
 PROTO = 'PROTO'
 
+SANITIZE_MAP = {
+    'VEVENT': sanitize_vevent,
+    'VTODO': sanitize_vtodo,
+}
+
 
 class EventType(IntEnum):
     DATE = 0
@@ -226,8 +232,8 @@ def update(self, vevent_str: str, href: str, etag: str='', calendar: str=None) -
                 "If you want to import it, please use `khal import FILE`."
             )
             raise NonUniqueUID
-        vevents = (sanitize_vevent(c, self.locale['default_timezone'], href, calendar) for
-                   c in ical.walk() if c.name == 'VEVENT')
+        vevents = (SANITIZE_MAP[c.name](c, self.locale['default_timezone'], href, calendar) for
+                   c in ical.walk() if c.name in SANITIZE_MAP.keys())
         # Need to delete the whole event in case we are updating a
         # recurring event with an event which is either not recurring any
         # more or has EXDATEs, as those would be left in the recursion
diff --git a/khal/khalendar/event.py b/khal/khalendar/event.py
index 69874a5d3..4eafb6f42 100644
--- a/khal/khalendar/event.py
+++ b/khal/khalendar/event.py
@@ -151,7 +151,7 @@ def fromVEvents(cls, events_list, ref=None, **kwargs):
     @classmethod
     def fromString(cls, event_str, ref=None, **kwargs):
         calendar_collection = cal_from_ics(event_str)
-        events = [item for item in calendar_collection.walk() if item.name == 'VEVENT']
+        events = [item for item in calendar_collection.walk() if item.name in ['VEVENT', 'VTODO']]
         return cls.fromVEvents(events, ref, **kwargs)
 
     def __lt__(self, other):
@@ -277,7 +277,8 @@ def symbol_strings(self):
                 'range': '\N{Left right arrow}',
                 'range_end': '\N{Rightwards arrow to bar}',
                 'range_start': '\N{Rightwards arrow from bar}',
-                'right_arrow': '\N{Rightwards arrow}'
+                'right_arrow': '\N{Rightwards arrow}',
+                'task': '\N{Pencil}',
             }
         else:
             return {
@@ -286,7 +287,8 @@ def symbol_strings(self):
                 'range': '<->',
                 'range_end': '->|',
                 'range_start': '|->',
-                'right_arrow': '->'
+                'right_arrow': '->',
+                'task': '(T)',
             }
 
     @property
@@ -304,6 +306,24 @@ def start(self):
         """this should return the start date(time) as saved in the event"""
         return self._start
 
+    @property
+    def task(self):
+        """this should return whether or not we are representing a task"""
+        return self._vevents[self.ref].name == 'VTODO'
+
+    @property
+    def task_status(self):
+        """nice representation of a task status"""
+        vstatus = self._vevents[self.ref].get('STATUS', 'NEEDS-ACTION')
+        status = ' '
+        if vstatus == 'COMPLETED':
+            status = 'X'
+        elif vstatus == 'IN-PROGRESS':
+            status = '/'
+        elif vstatus == 'CANCELLED':
+            status = '-'
+        return status
+
     @property
     def end(self):
         """this should return the end date(time) as saved in the event or
@@ -427,7 +447,10 @@ def summary(self):
                 name=name, number=number, suffix=suffix, desc=description, leap=leap,
             )
         else:
-            return self._vevents[self.ref].get('SUMMARY', '')
+            summary = self._vevents[self.ref].get('SUMMARY', '')
+            if self.task:
+                summary = f'[{self.task_status}] {summary}'
+            return summary
 
     def update_summary(self, summary):
         self._vevents[self.ref]['SUMMARY'] = summary
@@ -516,6 +539,14 @@ def _alarm_str(self):
             alarmstr = ''
         return alarmstr
 
+    @property
+    def _task_str(self):
+        if self.task:
+            taskstr = ' ' + self.symbol_strings['task']
+        else:
+            taskstr = ''
+        return taskstr
+
     def format(self, format_string, relative_to, env=None, colors=True):
         """
         :param colors: determines if colors codes should be printed or not
@@ -642,6 +673,7 @@ def format(self, format_string, relative_to, env=None, colors=True):
         attributes["repeat-symbol"] = self._recur_str
         attributes["repeat-pattern"] = self.recurpattern
         attributes["alarm-symbol"] = self._alarm_str
+        attributes["task-symbol"] = self._task_str
         attributes["title"] = self.summary
         attributes["organizer"] = self.organizer.strip()
         attributes["description"] = self.description.strip()
@@ -730,7 +762,8 @@ class LocalizedEvent(DatetimeEvent):
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
         try:
-            starttz = getattr(self._vevents[self.ref]['DTSTART'].dt, 'tzinfo', None)
+            sattr = 'DUE' if self.task else 'DTSTART'
+            starttz = getattr(self._vevents[self.ref][sattr].dt, 'tzinfo', None)
         except KeyError:
             msg = (
                 f"Cannot understand event {kwargs.get('href')} from "
diff --git a/khal/settings/khal.spec b/khal/settings/khal.spec
index b22610598..47990cf4e 100644
--- a/khal/settings/khal.spec
+++ b/khal/settings/khal.spec
@@ -273,7 +273,7 @@ bold_for_light_color = boolean(default=True)
 # ignored in `ikhal`, where events will always be shown in the color of the
 # calendar they belong to.
 # The syntax is the same as for :option:`--format`.
-agenda_event_format = string(default='{calendar-color}{cancelled}{start-end-time-style} {title}{repeat-symbol}{alarm-symbol}{description-separator}{description}{reset}')
+agenda_event_format = string(default='{calendar-color}{cancelled}{start-end-time-style} {title}{repeat-symbol}{alarm-symbol}{task-symbol}{description-separator}{description}{reset}')
 
 # Specifies how each *day header* is formatted.
 agenda_day_format = string(default='{bold}{name}, {date-long}{reset}')
@@ -288,7 +288,7 @@ monthdisplay = monthdisplay(default='firstday')
 # but :command:`list` and :command:`calendar`. It is therefore probably a
 # sensible choice to include the start- and end-date.
 # The syntax is the same as for :option:`--format`.
-event_format = string(default='{calendar-color}{cancelled}{start}-{end} {title}{repeat-symbol}{alarm-symbol}{description-separator}{description}{reset}')
+event_format = string(default='{calendar-color}{cancelled}{start}-{end} {title}{repeat-symbol}{alarm-symbol}{task-symbol}{description-separator}{description}{reset}')
 
 # When highlight_event_days is enabled, this section specifies how
 # the highlighting/coloring of days is handled.