diff --git a/aleksis/apps/kolego/migrations/0005_fix_overlapping_absences.py b/aleksis/apps/kolego/migrations/0005_fix_overlapping_absences.py
new file mode 100644
index 0000000000000000000000000000000000000000..244567acd1a253c385493dc7da5ff06f40765945
--- /dev/null
+++ b/aleksis/apps/kolego/migrations/0005_fix_overlapping_absences.py
@@ -0,0 +1,96 @@
+# Generated by Django 4.2.13 on 2024-07-16 11:01
+from datetime import datetime, time
+
+from django.db import migrations, models
+from django.db.models import Q
+
+
+def _run_forward(apps, schema_editor):
+    Absence = apps.get_model("kolego", "Absence")
+    for absence in Absence.objects.order_by("-datetime_start", "-date_start"):
+        # Convert dates of new event to datetimes in case dates are used
+        new_datetime_start = (
+            absence.datetime_start
+            if absence.datetime_start
+            else datetime.combine(absence.date_start, time.min)
+        ).astimezone(absence.timezone)
+        new_datetime_end = (
+            absence.datetime_end
+            if absence.datetime_end
+            else datetime.combine(absence.date_end, time.max)
+        ).astimezone(absence.timezone)
+
+        events_within = Absence.objects.filter(
+            person=absence.person,
+        ).filter(
+            Q(datetime_start__lte=new_datetime_end, datetime_end__gte=new_datetime_start)
+            | Q(date_start__lte=new_datetime_end.date(), date_end__gte=new_datetime_start.date())
+        )
+
+        for event_within in events_within:
+            event_within_datetime_start = (
+                event_within.datetime_start
+                if event_within.datetime_start
+                else datetime.combine(event_within.date_start, time.min)
+            )
+            event_within_datetime_end = (
+                event_within.datetime_end
+                if event_within.datetime_end
+                else datetime.combine(event_within.date_end, time.max)
+            )
+
+            # If overlapping absence has the same reason, just extend it
+            if event_within.reason == absence.reason:
+                event_within.datetime_start = min(new_datetime_start, event_within_datetime_start)
+                event_within.datetime_end = max(new_datetime_end, event_within_datetime_end)
+                event_within.save()
+            else:
+                if (
+                    new_datetime_start > event_within_datetime_start
+                    and new_datetime_end < event_within_datetime_end
+                ):
+                    # Cut existing event in two parts
+                    # First, cut end date of existing one
+                    event_within.datetime_end = new_datetime_start
+                    event_within.save()
+                    # Then, create new event based on existing one filling up the remaining time
+                    end_filler_event = event_within
+                    end_filler_event.pk = None
+                    end_filler_event.id = None
+                    end_filler_event.calendarevent_ptr_id = None
+                    end_filler_event.freebusy_ptr_id = None
+                    end_filler_event._state.adding = True
+                    end_filler_event.datetime_start = new_datetime_end
+                    end_filler_event.datetime_end = event_within_datetime_end
+
+                    end_filler_event.save()
+                elif (
+                    new_datetime_start <= event_within_datetime_start
+                    and new_datetime_end >= event_within_datetime_end
+                ):
+                    # Delete existing event
+                    event_within.delete()
+                elif (
+                    new_datetime_start > event_within_datetime_start
+                    and new_datetime_start < event_within_datetime_end
+                    and new_datetime_end >= event_within_datetime_end
+                ):
+                    # Cut end of existing event
+                    event_within.datetime_end = new_datetime_start
+                    event_within.save()
+                elif (
+                    new_datetime_start <= event_within_datetime_start
+                    and new_datetime_end < event_within_datetime_end
+                    and new_datetime_end > event_within_datetime_start
+                ):
+                    # Cut start of existing event
+                    event_within.datetime_start = new_datetime_end
+                    event_within.save()
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ("kolego", "0004_absencereasontag_absencereason_tags"),
+    ]
+
+    operations = [migrations.RunPython(_run_forward)]
diff --git a/aleksis/apps/kolego/models/absence.py b/aleksis/apps/kolego/models/absence.py
index a2c58927324c462dbbdfc6f9a1658924a8974d78..da1326d07909f3833171ea9e3d468637f2aaa3ba 100644
--- a/aleksis/apps/kolego/models/absence.py
+++ b/aleksis/apps/kolego/models/absence.py
@@ -1,3 +1,5 @@
+from datetime import date, datetime, time
+
 from django.db import models
 from django.db.models import Q, QuerySet
 from django.http import HttpRequest
@@ -9,7 +11,7 @@ from aleksis.core.managers import (
     RecurrencePolymorphicManager,
 )
 from aleksis.core.mixins import ExtensibleModel
-from aleksis.core.models import FreeBusy
+from aleksis.core.models import FreeBusy, Person
 
 from ..managers import AbsenceQuerySet
 
@@ -145,6 +147,136 @@ class Absence(FreeBusy):
     def __str__(self):
         return f"{self.person} ({self.datetime_start} - {self.datetime_end})"
 
+    def save(self, *args, skip_overlap_handling: bool = False, **kwargs):
+        extended_absence = None
+        modified_absences = []
+
+        if not skip_overlap_handling:
+            # Convert dates of new event to datetimes in case dates are used
+            new_datetime_start = (
+                self.datetime_start
+                if self.datetime_start
+                else datetime.combine(self.date_start, time.min)
+            ).astimezone(self.timezone)
+            new_datetime_end = (
+                self.datetime_end
+                if self.datetime_end
+                else datetime.combine(self.date_end, time.max)
+            ).astimezone(self.timezone)
+
+            events_within = Absence.get_objects(
+                None,
+                {"person": self.person.pk},
+                start=new_datetime_start,
+                end=new_datetime_end,
+            )
+
+            for event_within in events_within:
+                event_within_datetime_start = (
+                    event_within.datetime_start
+                    if event_within.datetime_start
+                    else datetime.combine(event_within.date_start, time.min)
+                )
+                event_within_datetime_end = (
+                    event_within.datetime_end
+                    if event_within.datetime_end
+                    else datetime.combine(event_within.date_end, time.max)
+                )
+
+                # If overlapping absence has the same reason, just extend it
+                if event_within.reason == self.reason:
+                    event_within.datetime_start = min(
+                        new_datetime_start, event_within_datetime_start
+                    )
+                    event_within.datetime_end = max(new_datetime_end, event_within_datetime_end)
+                    event_within.save(skip_overlap_handling=True)
+                    extended_absence = event_within
+                    modified_absences.append(event_within)
+                else:
+                    if (
+                        new_datetime_start > event_within_datetime_start
+                        and new_datetime_end < event_within_datetime_end
+                    ):
+                        # Cut existing event in two parts
+                        # First, cut end date of existing one
+                        event_within.datetime_end = new_datetime_start
+                        event_within.save(skip_overlap_handling=True)
+                        modified_absences.append(event_within)
+                        # Then, create new event based on existing one filling up the remaining time
+                        end_filler_event = event_within
+                        end_filler_event.pk = None
+                        end_filler_event.id = None
+                        end_filler_event.calendarevent_ptr_id = None
+                        end_filler_event.freebusy_ptr_id = None
+                        end_filler_event._state.adding = True
+                        end_filler_event.datetime_start = new_datetime_end
+                        end_filler_event.datetime_end = event_within_datetime_end
+
+                        end_filler_event.save(skip_overlap_handling=True)
+                        modified_absences.append(end_filler_event)
+                    elif (
+                        new_datetime_start <= event_within_datetime_start
+                        and new_datetime_end >= event_within_datetime_end
+                    ):
+                        # Delete existing event
+                        if event_within in modified_absences:
+                            modified_absences.remove(event_within)
+                        event_within.delete()
+                    elif (
+                        new_datetime_start > event_within_datetime_start
+                        and new_datetime_start < event_within_datetime_end
+                        and new_datetime_end >= event_within_datetime_end
+                    ):
+                        # Cut end of existing event
+                        event_within.datetime_end = new_datetime_start
+                        event_within.save(skip_overlap_handling=True)
+                        modified_absences.append(event_within)
+                    elif (
+                        new_datetime_start <= event_within_datetime_start
+                        and new_datetime_end < event_within_datetime_end
+                        and new_datetime_end > event_within_datetime_start
+                    ):
+                        # Cut start of existing event
+                        event_within.datetime_start = new_datetime_end
+                        event_within.save(skip_overlap_handling=True)
+                        modified_absences.append(event_within)
+
+                    modified_absences.append(self)
+
+        if extended_absence is not None:
+            self._extended_absence = extended_absence
+        else:
+            super().save(*args, **kwargs)
+        self._modified_absences = modified_absences
+
+    @classmethod
+    def get_for_person_by_datetimes(
+        cls,
+        person: Person,
+        datetime_start: datetime | None = None,
+        datetime_end: datetime | None = None,
+        date_start: date | None = None,
+        date_end: date | None = None,
+        defaults: dict | None = None,
+        **kwargs,
+    ) -> "Absence":
+        data = {"person": person, **kwargs}
+
+        if date_start:
+            data["date_start"] = date_start
+            data["date_end"] = date_end
+        else:
+            data["datetime_start"] = datetime_start
+            data["datetime_end"] = datetime_end
+
+        absence, created = cls.objects.get_or_create(**data, defaults=defaults)
+
+        if created:
+            if hasattr(absence, "_extended_absence"):
+                return absence._extended_absence
+            return absence
+        return absence
+
     class Meta:
         verbose_name = _("Absence")
         verbose_name_plural = _("Absences")
diff --git a/aleksis/apps/kolego/schema/absence.py b/aleksis/apps/kolego/schema/absence.py
index 775725f6218ec41df9a480a41365b0c8705333ed..bb5f2988e51cd263d90e653192f1b8b7df2ca2e6 100644
--- a/aleksis/apps/kolego/schema/absence.py
+++ b/aleksis/apps/kolego/schema/absence.py
@@ -87,6 +87,15 @@ class AbsenceBatchCreateMutation(BaseBatchCreateMutation):
         optional_fields = ("comment", "reason")
         permissions = ("kolego.create_absence_rule",)
 
+    @classmethod
+    def before_save(cls, root, info, input, created_objs):  # noqa
+        modified_absences = []
+
+        for absence in created_objs:
+            modified_absences += getattr(absence, "_modified_absences", [])
+
+        return modified_absences
+
 
 class AbsenceBatchDeleteMutation(BaseBatchDeleteMutation):
     class Meta:
@@ -109,6 +118,16 @@ class AbsenceBatchPatchMutation(BaseBatchPatchMutation):
         )
         permissions = ("kolego.edit_absence_rule",)
 
+    @classmethod
+    def after_mutate(cls, root, info, input, updated_objs, return_data):  # noqa
+        modified_absences = []
+
+        for absence in updated_objs:
+            modified_absences += getattr(absence, "_modified_absences", [])
+
+        return_data[cls._meta.return_field_name] = modified_absences
+        return return_data
+
 
 class AbsenceReasonBatchCreateMutation(BaseBatchCreateMutation):
     class Meta:
diff --git a/aleksis/apps/kolego/tests/models/test_absence.py b/aleksis/apps/kolego/tests/models/test_absence.py
new file mode 100644
index 0000000000000000000000000000000000000000..8ec8660c7687f5008cf60f652c29b1bf495bae2e
--- /dev/null
+++ b/aleksis/apps/kolego/tests/models/test_absence.py
@@ -0,0 +1,148 @@
+from datetime import datetime, timezone
+
+import pytest
+from aleksis.apps.kolego.models import Absence, AbsenceReason
+from aleksis.core.models import Person
+
+from zoneinfo import ZoneInfo
+
+pytestmark = pytest.mark.django_db
+
+
+@pytest.fixture
+def absences_test_data():
+    datetime_start_existing_1 = datetime(2024, 1, 1, 12, 0, tzinfo=timezone.utc).astimezone(ZoneInfo("Europe/Berlin"))
+    datetime_end_existing_1 = datetime(2024, 1, 2, 10, 0, tzinfo=timezone.utc).astimezone(ZoneInfo("Europe/Berlin"))
+    datetime_start_existing_2 = datetime(2024, 1, 2, 12, 0, tzinfo=timezone.utc).astimezone(ZoneInfo("Europe/Berlin"))
+    datetime_end_existing_2 = datetime(2024, 1, 3, 19, 43, tzinfo=timezone.utc).astimezone(ZoneInfo("Europe/Berlin"))
+
+    reason_1 = AbsenceReason.objects.create(short_name="i", name="ill")
+    reason_2 = AbsenceReason.objects.create(short_name="e", name="excused")
+    reason_1.refresh_from_db()
+    reason_2.refresh_from_db()
+
+    person_1 = Person.objects.create(first_name="Maria", last_name="Montessori")
+    person_2 = Person.objects.create(first_name="Karl Moritz", last_name="Fleischer")
+    person_1.refresh_from_db()
+    person_2.refresh_from_db()
+
+    # Create existing absencess
+    existing_absence_1 = Absence.objects.create(datetime_start=datetime_start_existing_1, datetime_end=datetime_end_existing_1, person=person_1, reason=reason_1)
+    existing_absence_2 = Absence.objects.create(datetime_start=datetime_start_existing_2, datetime_end=datetime_end_existing_2, person=person_1, reason=reason_1)
+    existing_absence_1.refresh_from_db()
+    existing_absence_2.refresh_from_db()
+
+    return {
+        "datetime_start_existing_1": datetime_start_existing_1,
+        "datetime_end_existing_1": datetime_end_existing_1,
+        "datetime_start_existing_2": datetime_start_existing_2,
+        "datetime_end_existing_2": datetime_end_existing_2,
+        "reason_1": reason_1,
+        "reason_2": reason_2,
+        "person_1": person_1,
+        "person_2": person_2,
+        "existing_absence_1": existing_absence_1,
+        "existing_absence_2": existing_absence_2,
+    }
+
+
+# Create new absences in six scenarios in relation to existing absence 1 with same person and different absence reason
+
+# 1: new absence is fully inside exiting absence
+def test_overlapping_single_absence_different_reason_inside(absences_test_data):
+    datetime_start_new = datetime(2024, 1, 1, 14, 0, tzinfo=timezone.utc).astimezone(ZoneInfo("Europe/Berlin"))
+    datetime_end_new = datetime(2024, 1, 2, 8, 0, tzinfo=timezone.utc).astimezone(ZoneInfo("Europe/Berlin"))
+    new_absence = Absence.objects.create(datetime_start=datetime_start_new, datetime_end=datetime_end_new, person=absences_test_data["person_1"], reason=absences_test_data["reason_2"])
+    new_absence.refresh_from_db()
+
+    absences_test_data["existing_absence_1"].refresh_from_db()
+
+    assert absences_test_data["existing_absence_1"].datetime_start == datetime(2024, 1, 1, 12, 0, tzinfo=timezone.utc)
+    assert absences_test_data["existing_absence_1"].datetime_end == datetime(2024, 1, 1, 14, 0, tzinfo=timezone.utc)
+
+    assert new_absence.datetime_start == datetime(2024, 1, 1, 14, 0, tzinfo=timezone.utc)
+    assert new_absence.datetime_end == datetime(2024, 1, 2, 8, 0, tzinfo=timezone.utc)
+
+    assert len(Absence.objects.all()) == 4
+    assert Absence.objects.filter(datetime_start=datetime(2024, 1, 2, 8, 0, tzinfo=timezone.utc)).exists()
+    
+    existing_absence_1_part_2 = Absence.objects.get(datetime_start=datetime(2024, 1, 2, 8, 0, tzinfo=timezone.utc))
+
+    assert existing_absence_1_part_2.datetime_start == datetime(2024, 1, 2, 8, 0, tzinfo=timezone.utc)
+    assert existing_absence_1_part_2.datetime_end == datetime(2024, 1, 2, 10, 0, tzinfo=timezone.utc)
+
+
+# 2: new absence covers the same time span as the existing one
+def test_overlapping_single_absence_different_reason_same(absences_test_data):
+    datetime_start_new = absences_test_data["datetime_start_existing_1"]
+    datetime_end_new = absences_test_data["datetime_end_existing_1"]
+    new_absence = Absence.objects.create(datetime_start=datetime_start_new, datetime_end=datetime_end_new, person=absences_test_data["person_1"], reason=absences_test_data["reason_2"])
+    new_absence.refresh_from_db()
+
+    assert len(Absence.objects.all()) == 2
+    assert not Absence.objects.filter(datetime_start=datetime(2024, 1, 1, 12, 0, tzinfo=timezone.utc), datetime_end=datetime(2024, 1, 2, 10, 0, tzinfo=timezone.utc)).exclude(pk=new_absence.pk).exists()
+
+    assert new_absence.datetime_start == datetime(2024, 1, 1, 12, 0, tzinfo=timezone.utc)
+    assert new_absence.datetime_end == datetime(2024, 1, 2, 10, 0, tzinfo=timezone.utc)
+
+
+# 3: new absence starts earlier and ends later than the existing absence
+def test_overlapping_single_absence_different_reason_exceed(absences_test_data):
+    datetime_start_new = datetime(2024, 1, 1, 10, 0, tzinfo=timezone.utc).astimezone(ZoneInfo("Europe/Berlin"))
+    datetime_end_new = datetime(2024, 1, 2, 12, 0, tzinfo=timezone.utc).astimezone(ZoneInfo("Europe/Berlin"))
+    new_absence = Absence.objects.create(datetime_start=datetime_start_new, datetime_end=datetime_end_new, person=absences_test_data["person_1"], reason=absences_test_data["reason_2"])
+    new_absence.refresh_from_db()
+
+    assert len(Absence.objects.all()) == 2
+    assert not Absence.objects.filter(datetime_start=datetime(2024, 1, 1, 12, 0, tzinfo=timezone.utc), datetime_end=datetime(2024, 1, 2, 10, 0, tzinfo=timezone.utc)).exists()
+
+    assert new_absence.datetime_start == datetime(2024, 1, 1, 10, 0, tzinfo=timezone.utc)
+    assert new_absence.datetime_end == datetime(2024, 1, 2, 12, 0, tzinfo=timezone.utc)
+
+
+# 4: new absence starts before existing absence and ends within the existing absence
+def test_overlapping_single_absence_different_reason_cut_start(absences_test_data):
+    datetime_start_new = datetime(2024, 1, 1, 10, 0, tzinfo=timezone.utc).astimezone(ZoneInfo("Europe/Berlin"))
+    datetime_end_new = datetime(2024, 1, 1, 14, 0, tzinfo=timezone.utc).astimezone(ZoneInfo("Europe/Berlin"))
+    new_absence = Absence.objects.create(datetime_start=datetime_start_new, datetime_end=datetime_end_new, person=absences_test_data["person_1"], reason=absences_test_data["reason_2"])
+    new_absence.refresh_from_db()
+
+    absences_test_data["existing_absence_1"].refresh_from_db()
+
+    assert absences_test_data["existing_absence_1"].datetime_start == datetime(2024, 1, 1, 14, 0, tzinfo=timezone.utc)
+    assert absences_test_data["existing_absence_1"].datetime_end == datetime(2024, 1, 2, 10, 0, tzinfo=timezone.utc)
+
+    assert new_absence.datetime_start == datetime(2024, 1, 1, 10, 0, tzinfo=timezone.utc)
+    assert new_absence.datetime_end == datetime(2024, 1, 1, 14, 0, tzinfo=timezone.utc)
+
+
+# 5: new absence starts within existing absence and ends after the existing absence
+def test_overlapping_single_absence_different_reason_cut_end(absences_test_data):
+    datetime_start_new = datetime(2024, 1, 2, 8, 0, tzinfo=timezone.utc).astimezone(ZoneInfo("Europe/Berlin"))
+    datetime_end_new = datetime(2024, 1, 2, 12, 0, tzinfo=timezone.utc).astimezone(ZoneInfo("Europe/Berlin"))
+    new_absence = Absence.objects.create(datetime_start=datetime_start_new, datetime_end=datetime_end_new, person=absences_test_data["person_1"], reason=absences_test_data["reason_2"])
+    new_absence.refresh_from_db()
+
+    absences_test_data["existing_absence_1"].refresh_from_db()
+
+    assert absences_test_data["existing_absence_1"].datetime_start == datetime(2024, 1, 1, 12, 0, tzinfo=timezone.utc)
+    assert absences_test_data["existing_absence_1"].datetime_end == datetime(2024, 1, 2, 8, 0, tzinfo=timezone.utc)
+
+    assert new_absence.datetime_start == datetime(2024, 1, 2, 8, 0, tzinfo=timezone.utc)
+    assert new_absence.datetime_end == datetime(2024, 1, 2, 12, 0, tzinfo=timezone.utc)
+
+
+# 6: new absence is completely outside of existing absence
+def test_overlapping_single_absence_different_reason_outside(absences_test_data):
+    datetime_start_new = datetime(2024, 1, 1, 8, 0, tzinfo=timezone.utc).astimezone(ZoneInfo("Europe/Berlin"))
+    datetime_end_new = datetime(2024, 1, 1, 10, 0, tzinfo=timezone.utc).astimezone(ZoneInfo("Europe/Berlin"))
+    new_absence = Absence.objects.create(datetime_start=datetime_start_new, datetime_end=datetime_end_new, person=absences_test_data["person_1"], reason=absences_test_data["reason_2"])
+    new_absence.refresh_from_db()
+
+    absences_test_data["existing_absence_1"].refresh_from_db()
+
+    assert absences_test_data["existing_absence_1"].datetime_start == datetime(2024, 1, 1, 12, 0, tzinfo=timezone.utc)
+    assert absences_test_data["existing_absence_1"].datetime_end == datetime(2024, 1, 2, 10, 0, tzinfo=timezone.utc)
+
+    assert new_absence.datetime_start == datetime(2024, 1, 1, 8, 0, tzinfo=timezone.utc)
+    assert new_absence.datetime_end == datetime(2024, 1, 1, 10, 0, tzinfo=timezone.utc)