diff --git a/lib/cronjobs/garbage_collector.class.php b/lib/cronjobs/garbage_collector.class.php
index 5e7f911f21f436f80dd0d4ffb848a3e64244693c..7adb0cd38f8bfaf2ee564a2c06005924bbc5b7d8 100644
--- a/lib/cronjobs/garbage_collector.class.php
+++ b/lib/cronjobs/garbage_collector.class.php
@@ -176,5 +176,11 @@ class GarbageCollectorJob extends CronJob
         if ($removed > 0 && $parameters['verbose']) {
             printf(_('Gelöschte Terminblöcke: %u') . "\n", $removed);
         }
+
+        // Remove expired tfa tokens
+        TFAToken::deleteBySQL(
+            'mkdate < UNIX_TIMESTAMP() - ?',
+            [TFASecret::getGreatestValidityDuration()]
+        );
     }
 }
diff --git a/lib/models/TFASecret.php b/lib/models/TFASecret.php
index 29f42eb3fb2b527750f0493294fafcb7c3f93279..aa863946c9b76eedc0865833cd128b8383c00b61 100644
--- a/lib/models/TFASecret.php
+++ b/lib/models/TFASecret.php
@@ -58,6 +58,24 @@ class TFASecret extends SimpleORMap
         $t = self::TYPES[$type];
         return $t['window'] * $t['period'];
     }
+
+    /**
+     * Returns the greatest validity duration for all defined types.
+     *
+     * @return int
+     */
+    public static function getGreatestValidityDuration(): int
+    {
+        $validity_duration = 0;
+        foreach (self::TYPES as $type) {
+            $duration = $type['window'] * $type['period'];
+            if ($duration > $validity_duration) {
+                $validity_duration = $duration;
+            }
+        }
+        return $validity_duration;
+    }
+
     /**
      * Overwrites the SORM setNew() method. This will create the secret string.
      *