diff --git a/aleksis/core/mixins.py b/aleksis/core/mixins.py
index 6a54824665f516b7f974dc33f168977ad1856bcf..d2c8fc0539293eda74713fea0d84287851c940aa 100644
--- a/aleksis/core/mixins.py
+++ b/aleksis/core/mixins.py
@@ -20,7 +20,6 @@ from django.views.generic import CreateView, UpdateView
 from django.views.generic.edit import DeleteView, ModelFormMixin
 
 import reversion
-from dirtyfields import DirtyFieldsMixin
 from guardian.admin import GuardedModelAdmin
 from jsonstore.fields import IntegerField, JSONFieldMixin
 from material.base import Layout, LayoutNode
@@ -71,7 +70,7 @@ def _generate_one_to_one_proxy_property(field, subfield):
     return property(getter, setter)
 
 
-class ExtensibleModel(DirtyFieldsMixin, models.Model, metaclass=_ExtensibleModelBase):
+class ExtensibleModel(models.Model, metaclass=_ExtensibleModelBase):
     """Base model for all objects in AlekSIS apps.
 
     This base model ensures all objects in AlekSIS apps fulfill the
diff --git a/aleksis/core/models.py b/aleksis/core/models.py
index 4ad9ced8497b162cb6aa5ae9df735796e6a0dae2..93ad1ff096d5f2db93d0953a439352386b837fe4 100644
--- a/aleksis/core/models.py
+++ b/aleksis/core/models.py
@@ -23,6 +23,7 @@ from django.utils.translation import gettext_lazy as _
 import jsonstore
 from cache_memoize import cache_memoize
 from dynamic_preferences.models import PerInstancePreferenceModel
+from model_utils import FieldTracker
 from model_utils.models import TimeStampedModel
 from phonenumber_field.modelfields import PhoneNumberField
 from polymorphic.models import PolymorphicModel
@@ -261,13 +262,15 @@ class Person(ExtensibleModel):
         """Return the count of unread notifications for this person."""
         return self.unread_notifications.count()
 
+    user_info_tracker = FieldTracker(fields=("first_name", "last_name", "email"))
+
     def save(self, *args, **kwargs):
         # Determine all fields that were changed since last load
-        dirty = set(self.get_dirty_fields().keys())
+        dirty = bool(self.user_info_tracker.changed())
 
         super().save(*args, **kwargs)
 
-        if self.user and (set(("first_name", "last_name", "email")) & dirty):
+        if self.user and dirty:
             # Synchronise user fields to linked User object to keep it up to date
             self.user.first_name = self.first_name
             self.user.last_name = self.last_name
@@ -433,10 +436,12 @@ class Group(SchoolTermRelatedExtensibleModel):
         else:
             return f"{self.name} ({self.short_name})"
 
+    group_info_tracker = FieldTracker(fields=("name", "short_name", "members", "owners"))
+
     def save(self, force: bool = False, *args, **kwargs):
         # Determine state of object in relation to database
-        created = self.pk is not None
-        dirty = set(self.get_dirty_fields().keys())
+        created = self.pk is None
+        dirty = bool(self.group_info_tracker.changed())
 
         super().save(*args, **kwargs)
 
diff --git a/poetry.lock b/poetry.lock
index eab05f8e12d33bcdd0806fb4066020e9759572cd..06443174db81ce59e51c3af70f2c4b9e58647671 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -601,18 +601,6 @@ python-versions = ">=3.5"
 Django = ">=1.11"
 sqlparse = ">=0.2.0"
 
-[[package]]
-name = "django-dirtyfields"
-version = "1.5.0"
-description = "Tracking dirty fields on a Django model instance (actively maintained)"
-category = "main"
-optional = false
-python-versions = "*"
-
-[package.dependencies]
-Django = ">=1.11"
-pytz = ">=2015.7"
-
 [[package]]
 name = "django-dynamic-preferences"
 version = "1.10.1"
@@ -2370,7 +2358,7 @@ ldap = ["django-auth-ldap"]
 [metadata]
 lock-version = "1.1"
 python-versions = "^3.7"
-content-hash = "ec39224c68bc21b5d35743bf9deef5b5f4f2b77e8e74785318137b9bdd904fe3"
+content-hash = "fff497acb1f86363cd099c56eaf7fa063da88090b5d211d423e3b13c1e016a16"
 
 [metadata.files]
 alabaster = [
@@ -2607,9 +2595,6 @@ django-debug-toolbar = [
     {file = "django-debug-toolbar-2.2.tar.gz", hash = "sha256:eabbefe89881bbe4ca7c980ff102e3c35c8e8ad6eb725041f538988f2f39a943"},
     {file = "django_debug_toolbar-2.2-py3-none-any.whl", hash = "sha256:ff94725e7aae74b133d0599b9bf89bd4eb8f5d2c964106e61d11750228c8774c"},
 ]
-django-dirtyfields = [
-    {file = "django-dirtyfields-1.5.0.tar.gz", hash = "sha256:7d7e8cf2fa153057cb4b51b5262c6f5b4594f23cd651e2f41746614bedfdf6f6"},
-]
 django-dynamic-preferences = [
     {file = "django-dynamic-preferences-1.10.1.tar.gz", hash = "sha256:e4b2bb7b2563c5064ba56dd76441c77e06b850ff1466a386a1cd308909a6c7de"},
     {file = "django_dynamic_preferences-1.10.1-py2.py3-none-any.whl", hash = "sha256:9419fa925fd2cbb665269ae72059eb3058bf080913d853419b827e4e7a141902"},
diff --git a/pyproject.toml b/pyproject.toml
index caafe4264f675d385f4eae6c37fa09a786ab5f3a..37a8497170dc423a5040533f23cfed740ecbd036 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -95,7 +95,6 @@ importlib-metadata = {version = "^3.0.0", python = "<3.9"}
 django-model-utils = "^4.0.0"
 bs4 = "^0.0.1"
 django-extensions = "^3.1.1"
-django-dirtyfields = "^1.5.0"
 ipython = "^7.20.0"
 
 [tool.poetry.extras]