diff --git a/src/core/admin.py b/src/core/admin.py
index ca17a5ae5ded567cd7ddc7515dd428db6915ee88..729f8bbaecd9b6301f3d29ecd1f40b7acd73728d 100644
--- a/src/core/admin.py
+++ b/src/core/admin.py
@@ -4,7 +4,7 @@ from typing import Any
 from django.conf import settings
 from django.contrib import admin
 from django.contrib.admin import FieldListFilter
-from django.contrib.auth.admin import UserAdmin
+from django.contrib.auth.admin import GroupAdmin, UserAdmin
 from django.contrib.contenttypes.admin import GenericTabularInline
 from django.contrib.gis.admin import GISModelAdmin
 from django.db.models import F, QuerySet
@@ -50,6 +50,8 @@ from .models import (
     StaticPageNamespace,
     StaticPageRevision,
     TagItem,
+    Team,
+    TeamMember,
     UserBadge,
     UserCommunicationChannel,
     UserContact,
@@ -312,6 +314,24 @@ class ConferenceTagAdmin(admin.ModelAdmin):
     search_fields = ['slug', 'description']
 
 
+class TeamMemberInline(admin.TabularInline):
+    model = TeamMember
+    extra = 1
+
+
+class TeamAdmin(GroupAdmin):
+    model = Team
+    inlines = [TeamMemberInline]
+    list_display = ['name', 'description', 'require_staff', 'member_count']
+    list_display_links = ['name']
+    list_filter = ['require_staff']
+    readonly_fields = ['description_html']
+    search_fields = ['name', 'description']
+
+    def member_count(self, obj):
+        return obj.members.count()
+
+
 class TagsInline(GenericTabularInline):
     model = TagItem
     ct_field = 'target_type'
@@ -1175,6 +1195,7 @@ admin.site.register(DereferrerStats, DereferrerStatsAdmin)
 admin.site.register(ConferenceMember, ConferenceMemberAdmin)
 admin.site.register(ConferenceTag, ConferenceTagAdmin)
 admin.site.register(ConferenceTrack, ConferenceTrackAdmin)
+admin.site.register(Team, TeamAdmin)
 admin.site.register(Assembly, AssemblyAdmin)
 admin.site.register(ActivityLogEntry, ActivityLogEntryAdmin)
 admin.site.register(BadgeCategory, BadgeCategoryAdmin)
diff --git a/src/core/locale/de/LC_MESSAGES/django.po b/src/core/locale/de/LC_MESSAGES/django.po
index 511972e98c88d8be5bc60d296080f0b4878a5418..870d86a831ee091e6698274138985c13bf04b322 100644
--- a/src/core/locale/de/LC_MESSAGES/django.po
+++ b/src/core/locale/de/LC_MESSAGES/django.po
@@ -1978,6 +1978,54 @@ msgstr "erklärender Begleittext des Tags"
 msgid "ConferenceTag__description"
 msgstr "Beschreibung"
 
+msgid "TeamMember"
+msgstr "Teammitglied"
+
+msgid "TeamMembers"
+msgstr "Teammitglieder"
+
+msgid "TeamMember__can_manage"
+msgstr "Verwalter*in"
+
+msgid "TeamMember__can_manage__help"
+msgstr "Diese Person kann das Team verwalten, d.h. Beschreibung und Mitglieder anlegen, bearbeiten oder löschen."
+
+msgid "TeamMember__created"
+msgstr "Beigetreten"
+
+msgid "TeamMember__updated"
+msgstr "Aktualisiert"
+
+msgid "TeamMember__clean__cannot_remove_last_manager"
+msgstr "Dieses Mitglied ist das letzte mit Verwaltungsrechten, sie können nicht entfernt werden."
+
+msgid "TeamMember__delete__cannot_delete_last_manager"
+msgstr "Du kannst dieses Mitglied nicht entfernen, da es das letzte Mitglied mit Verwaltungsrechten ist."
+
+msgid "Team"
+msgstr ""
+
+msgid "Teams"
+msgstr ""
+
+msgid "Team__conference"
+msgstr "Konferenz"
+
+msgid "Team__description"
+msgstr "Beschreibung"
+
+msgid "Team__description__help"
+msgstr "öffentliche Information (Markdown unterstützt)."
+
+msgid "Team__description_html"
+msgstr "Das gerenderte HTML der Beschreibung"
+
+msgid "Team__require_staff"
+msgstr "Nur für Konferenz-Team Mitglieder"
+
+msgid "Team__require_staff__help"
+msgstr "Dieses Team ist nur für Konferenz-Team Mitglieder zugreifbar."
+
 msgid "ConferenceMemberTicket__token_wrong_conference"
 msgstr "Das Ticket ist nicht für diese Konferenz."
 
diff --git a/src/core/locale/en/LC_MESSAGES/django.po b/src/core/locale/en/LC_MESSAGES/django.po
index 5029d87d03aa6c56f0ae627a436d83cec6ea297b..e720fec65ffe5f9657ee463fecb45684488fcee2 100644
--- a/src/core/locale/en/LC_MESSAGES/django.po
+++ b/src/core/locale/en/LC_MESSAGES/django.po
@@ -1976,6 +1976,54 @@ msgstr "additional explanation of this tag"
 msgid "ConferenceTag__description"
 msgstr "description"
 
+msgid "TeamMember"
+msgstr "Team member"
+
+msgid "TeamMembers"
+msgstr "Team members"
+
+msgid "TeamMember__can_manage"
+msgstr "Manager"
+
+msgid "TeamMember__can_manage__help"
+msgstr "This person may manage the team, i.e. create/edit/delete description or members."
+
+msgid "TeamMember__created"
+msgstr "joined"
+
+msgid "TeamMember__updated"
+msgstr "updated"
+
+msgid "TeamMember__clean__cannot_remove_last_manager"
+msgstr "You cannot remove this members rights, as they are the last manager of the team"
+
+msgid "TeamMember__delete__cannot_delete_last_manager"
+msgstr "You cannot remove this member, as it is the last manager of the team"
+
+msgid "Team"
+msgstr ""
+
+msgid "Teams"
+msgstr ""
+
+msgid "Team__conference"
+msgstr "Conference"
+
+msgid "Team__description"
+msgstr "description"
+
+msgid "Team__description__help"
+msgstr "public information (markdown supported)."
+
+msgid "Team__description_html"
+msgstr "the html rendered from the description"
+
+msgid "Team__require_staff"
+msgstr "require staff status"
+
+msgid "Team__require_staff__help"
+msgstr "This team is only accessible to persons with the staff status."
+
 msgid "ConferenceMemberTicket__token_wrong_conference"
 msgstr "This ticket is for another conference."
 
diff --git a/src/core/migrations/0169_team_teammember.py b/src/core/migrations/0169_team_teammember.py
new file mode 100644
index 0000000000000000000000000000000000000000..e0dc86a3bb537d9b0805c9249501ca7383a16fcf
--- /dev/null
+++ b/src/core/migrations/0169_team_teammember.py
@@ -0,0 +1,173 @@
+# Generated by Django 5.1.3 on 2024-12-09 01:56
+
+import core.fields
+import django.contrib.auth.models
+import django.db.models.deletion
+import rules.contrib.models
+import uuid
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("auth", "0012_alter_user_first_name_max_length"),
+        ("core", "0168_alter_invitation_type"),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name="Team",
+            fields=[
+                (
+                    "group_ptr",
+                    models.OneToOneField(
+                        auto_created=True,
+                        on_delete=django.db.models.deletion.CASCADE,
+                        parent_link=True,
+                        primary_key=True,
+                        serialize=False,
+                        to="auth.group",
+                    ),
+                ),
+                (
+                    "uuid",
+                    models.UUIDField(default=uuid.uuid4, editable=False, unique=True),
+                ),
+                (
+                    "description",
+                    models.TextField(
+                        blank=True,
+                        help_text="Team__description__help",
+                        verbose_name="Team__description",
+                    ),
+                ),
+                (
+                    "description_de",
+                    models.TextField(
+                        blank=True,
+                        help_text="Team__description__help",
+                        null=True,
+                        verbose_name="Team__description",
+                    ),
+                ),
+                (
+                    "description_en",
+                    models.TextField(
+                        blank=True,
+                        help_text="Team__description__help",
+                        null=True,
+                        verbose_name="Team__description",
+                    ),
+                ),
+                (
+                    "description_html",
+                    models.TextField(
+                        blank=True, null=True, verbose_name="Team__description_html"
+                    ),
+                ),
+                (
+                    "description_html_de",
+                    models.TextField(
+                        blank=True, null=True, verbose_name="Team__description_html"
+                    ),
+                ),
+                (
+                    "description_html_en",
+                    models.TextField(
+                        blank=True, null=True, verbose_name="Team__description_html"
+                    ),
+                ),
+                (
+                    "require_staff",
+                    models.BooleanField(
+                        default=False,
+                        help_text="Team__require_staff__help",
+                        verbose_name="Team__require_staff",
+                    ),
+                ),
+                (
+                    "conference",
+                    core.fields.ConferenceReference(
+                        help_text="Conference__reference_help",
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="Teams",
+                        to="core.conference",
+                        verbose_name="Team__conference",
+                    ),
+                ),
+            ],
+            options={
+                "verbose_name": "Team",
+                "verbose_name_plural": "Teams",
+            },
+            bases=(rules.contrib.models.RulesModelMixin, "auth.group"),
+            managers=[
+                ("objects", django.contrib.auth.models.GroupManager()),
+            ],
+        ),
+        migrations.CreateModel(
+            name="TeamMember",
+            fields=[
+                (
+                    "id",
+                    models.UUIDField(
+                        default=uuid.uuid4,
+                        editable=False,
+                        primary_key=True,
+                        serialize=False,
+                    ),
+                ),
+                (
+                    "can_manage",
+                    models.BooleanField(
+                        default=False,
+                        help_text="TeamMember__can_manage__help",
+                        verbose_name="TeamMember__can_manage",
+                    ),
+                ),
+                (
+                    "created",
+                    models.DateTimeField(
+                        auto_now_add=True, verbose_name="TeamMember__created"
+                    ),
+                ),
+                (
+                    "updated",
+                    models.DateTimeField(
+                        auto_now=True, verbose_name="TeamMember__updated"
+                    ),
+                ),
+                (
+                    "team",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="members",
+                        to="core.team",
+                    ),
+                ),
+                (
+                    "user",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="teams",
+                        to=settings.AUTH_USER_MODEL,
+                    ),
+                ),
+            ],
+            options={
+                "verbose_name": "TeamMember",
+                "verbose_name_plural": "TeamMembers",
+            },
+            bases=(rules.contrib.models.RulesModelMixin, models.Model),
+        ),
+        migrations.AddIndex(
+            model_name="team",
+            index=models.Index(fields=["uuid"], name="core_team_uuid_4e6673_idx"),
+        ),
+        migrations.AlterUniqueTogether(
+            name="teammember",
+            unique_together={("user", "team")},
+        ),
+    ]
diff --git a/src/core/models/__init__.py b/src/core/models/__init__.py
index 7a064b14cfccba59e12b85c6b39a9744697f57ea..d51918ddfe17d6872ba448b9996b28cc47088523 100644
--- a/src/core/models/__init__.py
+++ b/src/core/models/__init__.py
@@ -1,4 +1,5 @@
 from core.models.invitation import Invitation
+from core.models.teams import Team, TeamMember
 
 from .activitylog import ActivityLogChange, ActivityLogEntry
 from .assemblies import Assembly, AssemblyLink, AssemblyMember
@@ -70,6 +71,8 @@ __all__ = [
     'StaticPageNamespace',
     'StaticPageRevision',
     'TagItem',
+    'Team',
+    'TeamMember',
     'UserBadge',
     'UserCommunicationChannel',
     'UserContact',
diff --git a/src/core/models/conference.py b/src/core/models/conference.py
index 02c7c5393dc9d8c22720afd1f4cd164fc07b7675..b84c50cb0048857d7e9ad66f6cc09ba484b5936f 100644
--- a/src/core/models/conference.py
+++ b/src/core/models/conference.py
@@ -62,6 +62,7 @@ class ConferenceMember(models.Model):
     is_staff = models.BooleanField(default=False)
     has_ticket = models.BooleanField(default=False, help_text=_('ConferenceMember--has_ticket--help'), verbose_name=_('ConferenceMember--has_ticket'))
 
+    # TODO: Remove this after 38c3, it will be replaced with teams
     permission_groups = models.ManyToManyField(Group, blank=True, related_name='+')
     static_page_groups = pg_fields.ArrayField(models.CharField(max_length=50), blank=True, null=True)
 
diff --git a/src/core/models/teams/__init__.py b/src/core/models/teams/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..4a4293f441aee1aac989bfd37b5cf93387d5dd26
--- /dev/null
+++ b/src/core/models/teams/__init__.py
@@ -0,0 +1,7 @@
+from core.models.teams.team_member import TeamMember
+from core.models.teams.teams import Team
+
+__all__ = [
+    'Team',
+    'TeamMember',
+]
diff --git a/src/core/models/teams/team_member.py b/src/core/models/teams/team_member.py
new file mode 100644
index 0000000000000000000000000000000000000000..5523f45c72873bbebfaba0d7436e686875c5ee8c
--- /dev/null
+++ b/src/core/models/teams/team_member.py
@@ -0,0 +1,73 @@
+from typing import TYPE_CHECKING, Any, TypeIs
+from uuid import uuid4
+
+from django.core.exceptions import ValidationError
+from django.db import models
+from django.utils.translation import gettext_lazy as _
+from rules.contrib.models import RulesModel
+from rules.predicates import is_superuser, predicate
+
+if TYPE_CHECKING:  # pragma: no cover
+    from core.models import PlatformUser
+
+
+class LastManagerError(ValidationError):
+    pass
+
+
+@predicate
+def has_invitation(user: 'PlatformUser', team_member: 'TeamMember | None' = None) -> bool:
+    if team_member is None:  # pragma: no cover
+        return False
+    return user.is_authenticated and team_member.team.sent_invitations.filter(requested_id=user.id).exists()
+
+
+@predicate
+def is_associated_user(user: 'PlatformUser', team_member: 'TeamMember | None' = None) -> bool:
+    if team_member is None:  # pragma: no cover
+        return False
+    return user.is_authenticated and team_member.user == user
+
+
+@predicate
+def is_team_manager(user: 'PlatformUser', team_member: 'TeamMember | None' = None) -> bool:
+    if team_member is None:  # pragma: no cover
+        return False
+    return user.is_authenticated and team_member.team.members.filter(user=user, can_manage=True).exists()
+
+
+class TeamMember(RulesModel):
+    class Meta:
+        rules_permissions = {
+            'add': is_team_manager | is_superuser | has_invitation,
+            'change': is_team_manager | is_superuser,
+            'delete': is_team_manager | is_superuser | is_associated_user,
+        }
+        unique_together = (('user', 'team'),)
+        verbose_name = _('TeamMember')
+        verbose_name_plural = _('TeamMembers')
+
+    id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
+    user = models.ForeignKey('PlatformUser', related_name='teams', on_delete=models.CASCADE)
+    team = models.ForeignKey('Team', related_name='members', on_delete=models.CASCADE)
+    can_manage = models.BooleanField(
+        default=False,
+        verbose_name=_('TeamMember__can_manage'),
+        help_text=_('TeamMember__can_manage__help'),
+    )
+    created = models.DateTimeField(auto_now_add=True, verbose_name=_('TeamMember__created'))
+    updated = models.DateTimeField(auto_now=True, verbose_name=_('TeamMember__updated'))
+
+    @classmethod
+    def type_is(cls, obj: object) -> TypeIs['TeamMember']:
+        return isinstance(obj, cls)
+
+    def clean(self) -> None:
+        if not self.can_manage and self.team.members.filter(can_manage=True).count() == 1:
+            raise LastManagerError(_('TeamMember__clean__cannot_remove_last_manager'))
+        return super().clean()
+
+    def delete(self, using: Any = None, keep_parents: bool = False) -> tuple[int, dict[str, int]]:
+        if self.can_manage and self.team.members.filter(can_manage=True).count() == 1:
+            raise LastManagerError(_('TeamMember__delete__cannot_delete_last_manager'))
+        return super().delete(using, keep_parents)
diff --git a/src/core/models/teams/teams.py b/src/core/models/teams/teams.py
new file mode 100644
index 0000000000000000000000000000000000000000..09bcf763cf15155f767bf7cd24bd8d3007667ac0
--- /dev/null
+++ b/src/core/models/teams/teams.py
@@ -0,0 +1,103 @@
+from typing import TYPE_CHECKING, TypeIs
+from uuid import uuid4
+
+from django.contrib.auth.models import Group
+from django.contrib.contenttypes.fields import GenericRelation
+from django.db import models
+from django.utils.translation import gettext_lazy as _
+from rules.contrib.models import RulesModelBase, RulesModelMixin
+from rules.predicates import is_superuser, predicate
+
+from core.fields import ConferenceReference
+from core.markdown import compile_translated_markdown_fields, store_relationships
+from core.models.invitation import Invitation
+from core.predicates import is_conference_staff
+
+if TYPE_CHECKING:  # pragma: no cover
+    from core.models import PlatformUser
+
+
+@predicate
+def is_team_member(user: 'PlatformUser', team: 'Team | None' = None) -> bool:
+    if team is None:  # pragma: no cover
+        return False
+    return user.is_authenticated and team.members.filter(user=user).exists()
+
+
+@predicate
+def is_team_manager(user: 'PlatformUser', team: 'Team | None' = None) -> bool:
+    if team is None:  # pragma: no cover
+        return False
+    return user.is_authenticated and team.members.filter(user=user, can_manage=True).exists()
+
+
+@predicate
+def is_public_team(user: 'PlatformUser', team: 'Team | None' = None) -> bool:
+    # If team is None, we are checking for the permission to view the list of teams, everybody can see it.
+    if team is None:
+        return True
+    return not team.require_staff
+
+
+class Team(RulesModelMixin, Group, metaclass=RulesModelBase):
+    class Meta:
+        rules_permissions = {
+            'view': is_conference_staff | is_public_team,
+            'view_details': is_team_member | is_superuser,
+            'add': is_superuser,
+            'change': is_team_manager | is_superuser,
+            'change_permissions': is_superuser,
+            'delete': is_superuser,
+            'can_join': is_conference_staff | is_public_team,
+        }
+        indexes = [
+            models.Index(fields=['uuid']),
+        ]
+        verbose_name = _('Team')
+        verbose_name_plural = _('Teams')
+
+    uuid = models.UUIDField(unique=True, default=uuid4, editable=False)
+    conference = ConferenceReference(
+        related_name='Teams',
+        verbose_name=_('Team__conference'),
+    )
+    description = models.TextField(
+        blank=True,
+        verbose_name=_('Team__description'),
+        help_text=_('Team__description__help'),
+    )
+    description_html = models.TextField(
+        blank=True,
+        null=True,
+        verbose_name=_('Team__description_html'),
+    )
+    # Is this ream only for staff members (e.g. assembly team)?
+    require_staff = models.BooleanField(
+        default=False,
+        verbose_name=_('Team__require_staff'),
+        help_text=_('Team__require_staff__help'),
+    )
+
+    sent_invitations = GenericRelation(
+        Invitation,
+        content_type_field='requester_type',
+        object_id_field='requester_id',
+        related_query_name='assemblies_inviters',
+    )
+    received_invitations = GenericRelation(
+        Invitation,
+        content_type_field='requested_type',
+        object_id_field='requested_id',
+        related_query_name='assemblies_invited',
+    )
+
+    @classmethod
+    def type_is(cls, obj: object) -> TypeIs['Team']:
+        return isinstance(obj, cls)
+
+    def save(self, *args, update_fields=None, **kwargs):
+        if update_fields is None or 'description' in update_fields:
+            render_results = compile_translated_markdown_fields(self, self.conference, 'description')
+            store_relationships(self.conference, self, render_results)
+
+        return super().save(*args, update_fields=update_fields, **kwargs)
diff --git a/src/core/translation.py b/src/core/translation.py
index 50d03e2324dae84738131c4f73a41a914c425ed5..fff800551ecfd179321550e6a5234c14f80b33a0 100644
--- a/src/core/translation.py
+++ b/src/core/translation.py
@@ -2,7 +2,12 @@ from django.contrib import admin
 from modeltranslation.admin import TranslationAdmin
 from modeltranslation.translator import TranslationOptions, register
 
-from core.admin import BadgeAdmin, BadgeCategoryAdmin, ProjectAdmin
+from core.admin import (
+    BadgeAdmin,
+    BadgeCategoryAdmin,
+    ProjectAdmin,
+    TeamAdmin,
+)
 from core.models import (
     Assembly,
     Badge,
@@ -16,6 +21,7 @@ from core.models import (
     MetaNavItem,
     Project,
     Room,
+    Team,
 )
 
 
@@ -92,6 +98,22 @@ class ConferenceMemberTranslationOptions(TranslationOptions):
     )
 
 
+@register(Team)
+class TeamTranslationOptions(TranslationOptions):
+    fields = (
+        'description',
+        'description_html',
+    )
+
+
+class TranslatedTeamAdmin(TeamAdmin, TranslationAdmin):
+    pass
+
+
+admin.site.unregister(Team)
+admin.site.register(Team, TranslatedTeamAdmin)
+
+
 @register(Room)
 class RoomTranslationOptions(TranslationOptions):
     fields = (